MQTTの負荷テストもバッチリ!!Locustを活用した分散負荷テスト環境の構築
はじめに
サーバーレス開発部@大阪の岩田です。 AWS IoTに対して負荷テストを行う機会があったのですが、負荷テストツールのLocustとECSを組み合わせることで、スケール可能な負荷テスト環境が構築できたので手順をご紹介します。
Locustとは
まず、Locustについて簡単にご紹介します。 LocustはPythonで記述された分散型の負荷テストツールです。
- Pythonのコードでテストシナリオが記述できる
- master - slave構成を取ることができ、slaveを増やすことで簡単にスケールアウトすることが可能
- WebのUIが提供されている
といった特徴があります。
WebのUIはこんなイメージです。
Locustを選定した理由
負荷テストのツールを選定する際に、Locustの対抗馬としてJMeterとGatlingが候補に上がっていたのですが、両ツールともMQTT OverTLSの負荷テストを行うのが困難で、Pythonの既存ライブラリを活用してテストシナリオを自由に記述できるLocustを採用しました。
テストシナリオ
下記のようなシナリオを想定していました。
- 数万台のデバイスが同時にAWS IoTに対してPublishする
- デバイスはMQTT OverTLSでAWS IoTに接続する
- Publishするトピックはデバイスごとに固有 例) foo/device0001/bar
- 数万台のブラウザが同時にAWS IoTに接続し、各デバイスがPublishしているトピックをSubscribeする
- ブラウザはMQTT Over WebsocketでAWS IoTに接続する
- ブラウザとデバイスは1:1で紐づく(pub - sub の組み合わせが数万セット出来上がる)
構築する環境
下記のような環境を構築します。
Publish用のクラスタ、Subscribe用のクラスタそれぞれにVPCを作成し、パブリックサブネット内にmasterを、プライベートサブネット内にslaveを配置します。 masterは手っ取り早くFargateで起動、slaveはFargateの上限に引っかからない様にEC2で起動します。
ソースコード
テストに使用したソースコードを見て行きます。 ※実際に使用したものから一部改変しています。
ソースコードはこちらのサイトを参考に実装しました。
記述のお作法については下記のサイトが参考になりました。
まず、ディレクトリ構成は下記の通りです。
. ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── VeriSign-Class\ 3-Public-Primary-Certification-Authority-G5.pem ├── cfn-template.yml ├── common.py ├── pub.py ├── sub.py ├── thing1-certificate.pem └── thing1-private.pem
thing1-private.pem
、thing1-certificate.pem
はそれぞれテストに使用するモノのクライアント証明書と秘密鍵です。
事前にAWS IoT上にモノを1つ登録し、クライアント証明書を発行しておきます。
今回は全クライアントが1つのモノを使い回す構成としました。
また、VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem
がAWS IoTのCA証明書になります。
※今後は、Amazon Trust ServicesのCA証明書を使用していく方が良いでしょう
AWS IoT Core がお客様に提供する Symantec の認証局無効化の対応方法
Pipfile
ライブラリの導入などはpipenvを利用しました。 最終的なPipfileの中身は下記の通りです。
[[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] locustio = "*" paho-mqtt = "*" awsiotpythonsdk = "*" "boto3" = "*" [dev-packages] [requires] python_version = "3.6"
共通処理
Publish側とSubscribeの共通ロジックを切り出しています。 プログラムの規模が大きくならないのが分かっているので、特にクラス化などもせずに、なんでもかんでも全て1ファイルに詰め込みました。 また、使い捨てのコードなので、エラーハンドリングはガッツリ省略しています。
import boto3 import os AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') AWS_DEFAULT_REGION = os.getenv('AWS_DEFAULT_REGION', 'us-east-1') IOT_ENDPOINT = os.getenv('IOT_ENDPOINT') TABLE_NAME = os.getenv('SEQ_TABLE') QoS = 1 CA_FILE_PATH = './VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem' DYNAMO = boto3.resource('dynamodb', region_name=AWS_DEFAULT_REGION) def get_topic(seq:int)->str: imei = '{0:07d}'.format(seq) return f'hoge/{imei}/fuga' def get_slave_no(mqtt_type:str)->int: table = DYNAMO.Table(TABLE_NAME) res = table.update_item( Key={ 'type': mqtt_type, }, UpdateExpression="set seq = seq + :val", ExpressionAttributeValues={ ':val': 1 }, ReturnValues="UPDATED_NEW" ) return res['Attributes']['seq']
ポイントとしてslaveがN台構成になった時に、シナリオを実行中のLocustコンテナ側から自分が何台目のslaveなのかを把握できるようにget_slave_no
という関数を作成しています。
この関数ではDynamoDBを利用して連番を採番しています。
下記のようなテーブルがあり、slaveが起動する度にSEQをカウントアップしていくようなイメージです。
type | seq |
---|---|
pub | 1 |
sub | 1 |
DynamoDBのキャパシティユニットをケチりたかったので、同一slave内での同時実行数に応じたSEQカウントアップはPythonのコードで行いました。 このコードだと、テストシナリオの同時ユーザー数がslave数で割り切れない場合は複数のユーザーが同一トピックにPub・Subすることになりますが、あまりパワーやコストをかけたくなかったので、割り切って諦めました。
Publish
次にPublish側のコードです。 こちらもエラーハンドリングはガッツリ省略しています。
# -*- coding: utf-8 -*- import gevent import json import time from locust import TaskSet, Locust, task, runners from locust.events import request_success, request_failure import ssl import paho.mqtt.client as mqtt import time import random import threading from common import * def get_client(): client = mqtt.Client(protocol=mqtt.MQTTv311) client.tls_set(CA_FILE_PATH, certfile='thing1-certificate.pem', keyfile='thing1-private.pem', tls_version=ssl.PROTOCOL_TLSv1_2) client.tls_insecure_set(True) return client class MQTTPubTaskSet(TaskSet): slave_no = 0 seq = 0 client = None topic = '' def setup(self): MQTTPubTaskSet.slave_no = get_slave_no('pub') lock = threading.Lock() lock.acquire() num_clients = runners.locust_runner.num_clients # 自分のスレーブID -1 台のスレーブがすでに起動しており、スレーブ1台あたりのクライアント数は均等に分散されているとする MQTTPubTaskSet.seq = num_clients * (MQTTPubTaskSet.slave_no - 1) + 1 lock.release() def on_start(self): self.client = get_client() lock = threading.Lock() lock.acquire() self.topic = get_topic(int(MQTTPubTaskSet.seq)) MQTTPubTaskSet.seq += 1 lock.release() self.client.connect(IOT_ENDPOINT, 8883, keepalive=60) self.client.loop_start() @task def pub(self): time.sleep(1) # 生データに加えて負荷テストでの集計用にtimestampを付与 payload = json.dumps({ "timestamp": time.time(), "topic": self.topic, "payload": { "hoge":"hogehoge" } }) start_time = time.time() err, mid = self.client.publish(self.topic, payload, qos=QoS) if err: request_failure.fire( request_type='publish', name=self.topic, response_time=(time.time() - start_time) * 1000, exception=err, ) return request_success.fire( request_type='publish', name=self.topic, response_time=(time.time() - start_time) * 1000, response_length=len(payload), ) class Devices(Locust): task_set = MQTTPubTaskSet min_wait = 1 max_wait = 1
MQTT OverTLSで接続するためにpaho mqttを使用しています。
なお、接続のために、事前にAWS Iot側でモノと証明書、ポリシーの登録を行う必要があります。
Subscribe
次にSubscribe側のコードです。 こちらもエラーハンドリングはガッツリ省略しています。
# -*- coding: utf-8 -*- import json import time import uuid import gevent from locust import TaskSet, Locust, runners, task from locust.events import request_success from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient import threading from common import * def on_receive(client, userdata, message): payload = json.loads(message.payload) elapsed = time.time() - payload['timestamp'] request_success.fire( request_type='sub', name=message.topic, response_time=elapsed * 1000, response_length=len(message.payload), ) def get_client(): client = AWSIoTMQTTClient(clientID=uuid.uuid4().hex, useWebsocket=True) client.configureIAMCredentials(AWSAccessKeyID=AWS_ACCESS_KEY_ID, AWSSecretAccessKey=AWS_SECRET_ACCESS_KEY) client.configureCredentials(CAFilePath=CA_FILE_PATH) client.configureEndpoint(hostName=IOT_ENDPOINT, portNumber=443) client.configureOfflinePublishQueueing(-1) client.configureDrainingFrequency(2) client.configureConnectDisconnectTimeout(120) client.configureMQTTOperationTimeout(60) return client class AWSIoTTaskSet(TaskSet): slave_no = 0 seq = 0 client = None clients = {} topic = '' def setup(self): AWSIoTTaskSet.slave_no = get_slave_no('sub') lock = threading.Lock() lock.acquire() num_clients = runners.locust_runner.num_clients # 自分のスレーブID -1 台のスレーブがすでに起動しており、スレーブ1台あたりのクライアント数は均等に分散されているとする AWSIoTTaskSet.seq = num_clients * (AWSIoTTaskSet.slave_no - 1) + 1 lock.release() def on_start(self): self.client = get_client() self.client.connect() lock = threading.Lock() lock.acquire() user_count = runners.locust_runner.user_count seq = AWSIoTTaskSet.seq AWSIoTTaskSet.seq += 1 lock.release() topic = get_topic(int(seq)) self.client.subscribe(topic, QoS, on_receive) while True: time.sleep(300) @task def dummy(self): # on_startの中で無限ループさせるため実際にこの処理は呼ばれないが、 # taskが無いとlocust的にエラーになるのでダミーのタスクを作成しておく print('----------dummy task called--------------') class AWSIoTUser(Locust): task_set = AWSIoTTaskSet min_wait = 1 max_wait = 1
Subscribe側はテスト終了までずっとSubscribeし続けておいて欲しいので、on_startの中でAWS Iotに接続した後は無限ループさせています。 また、タスクが無いと怒られるようなのでdummyというメソッドを作成しています。
こちらはMQTT Over Websocketで接続するためAWSIoTMQTTClientを利用しています。
Dockerfile
Dockerfileです。
FROM python:3.6 RUN pip install pipenv RUN mkdir /app WORKDIR /app ADD Pipfile /app/ ADD Pipfile.lock /app/ RUN LIBRARY_PATH=/lib:/usr/lib pipenv install --system --ignore-pipfile ADD . /app/
構築手順
実際に負荷テストの環境を構築していきます。
VPC等のリソース作成
下記のCloudFormationのテンプレートで構築しました。 EC2のインスタンスサイズは決め打ちでm5.largeにしているので、必要に応じて適宜修正します。
AWSTemplateFormatVersion: 2010-09-09 Description: Setup Stress Test Environment Parameters: UserGIP: Description: The IP address range that can be used to Locust WebUI Type: String MinLength: '9' MaxLength: '18' Default: 0.0.0.0/0 AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. Resources: PubVPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: 'true' EnableDnsHostnames: 'true' InstanceTenancy: default Tags: - Key: Name Value: locust Pub VPC PubVPCPublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref PubVPC Tags: - Key: Name Value: PubVPCPublicRouteTable PubVPCPrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref PubVPC Tags: - Key: Name Value: PubVPCPrivateRouteTable PubVPCPublicSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref PubVPC CidrBlock: 10.0.0.0/24 AvailabilityZone: us-east-1a MapPublicIpOnLaunch: true Tags: - Key: Name Value: PubVPCPublicSubnetA PubVPCPublicSubnetARouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PubVPCPublicSubnetA RouteTableId: !Ref PubVPCPublicRouteTable PubVPCPrivateSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref PubVPC CidrBlock: 10.0.1.0/24 AvailabilityZone: us-east-1a Tags: - Key: Name Value: PubVPCPrivateSubnetA PubVPCPrivateSubnetARouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PubVPCPrivateSubnetA RouteTableId: !Ref PubVPCPrivateRouteTable PubVPCInternetGateway: Type: AWS::EC2::InternetGateway PubVPCAttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref PubVPC InternetGatewayId: !Ref PubVPCInternetGateway PubVPCRoute: Type: AWS::EC2::Route DependsOn: PubVPCInternetGateway Properties: RouteTableId: !Ref PubVPCPublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref PubVPCInternetGateway PubVPCPrivateSubDefaultRoute: Type: AWS::EC2::Route DependsOn: PubVPCNatGateway Properties: RouteTableId: !Ref PubVPCPrivateRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref PubVPCNatGateway PubVPCNatGatewayEIP: Type: AWS::EC2::EIP Properties: Domain: vpc PubVPCNatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt PubVPCNatGatewayEIP.AllocationId SubnetId: !Ref PubVPCPublicSubnetA Tags: - Key: Name Value: PubVPCNatGateway PubVPCLocustSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow Access To Locust VpcId: !Ref PubVPC PubVPCLocustSecurityGroupIngressWebUIInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref PubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 SourceSecurityGroupId: !Ref PubVPCLocustSecurityGroup PubVPCLocustSecurityGroupIngressMasterSlaveInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref PubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5558 SourceSecurityGroupId: !Ref PubVPCLocustSecurityGroup PubVPCLocustSecurityGroupIngressWebUI: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref PubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 CidrIp: !Sub ${UserGIP} PubVPCLocustSecurityGroupIngressMasterSlave: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref PubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5557 CidrIp: !Sub ${UserGIP} PubVPCLocustSecurityGroupIngressSSH: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref PubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Sub ${UserGIP} SubVPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: 'true' EnableDnsHostnames: 'true' InstanceTenancy: default Tags: - Key: Name Value: locust Sub VPC SubVPCPublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref SubVPC Tags: - Key: Name Value: SubVPCPublicRouteTable SubVPCPrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref SubVPC Tags: - Key: Name Value: SubVPCPrivateRouteTable SubVPCPublicSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref SubVPC CidrBlock: 10.0.0.0/24 AvailabilityZone: us-east-1a MapPublicIpOnLaunch: true Tags: - Key: Name Value: SubVPCPublicSubnetA SubVPCPublicSubnetARouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubVPCPublicSubnetA RouteTableId: !Ref SubVPCPublicRouteTable SubVPCPrivateSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref SubVPC CidrBlock: 10.0.1.0/24 AvailabilityZone: us-east-1a Tags: - Key: Name Value: SubVPCPrivateSubnetA SubVPCPrivateSubnetARouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubVPCPrivateSubnetA RouteTableId: !Ref SubVPCPrivateRouteTable SubVPCInternetGateway: Type: AWS::EC2::InternetGateway SubVPCAttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref SubVPC InternetGatewayId: !Ref SubVPCInternetGateway SubVPCRoute: Type: AWS::EC2::Route DependsOn: SubVPCInternetGateway Properties: RouteTableId: !Ref SubVPCPublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref SubVPCInternetGateway SubVPCPrivateSubDefaultRoute: Type: AWS::EC2::Route DependsOn: SubVPCNatGateway Properties: RouteTableId: !Ref SubVPCPrivateRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref SubVPCNatGateway SubVPCNatGatewayEIP: Type: AWS::EC2::EIP Properties: Domain: vpc SubVPCNatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt SubVPCNatGatewayEIP.AllocationId SubnetId: !Ref SubVPCPublicSubnetA Tags: - Key: Name Value: SubVPCNatGateway SubVPCLocustSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow Access To Locust VpcId: !Ref SubVPC SubVPCLocustSecurityGroupIngressWebUIInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref SubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 SourceSecurityGroupId: !Ref SubVPCLocustSecurityGroup SubVPCLocustSecurityGroupIngressMasterSlaveInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref SubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5558 SourceSecurityGroupId: !Ref SubVPCLocustSecurityGroup SubVPCLocustSecurityGroupIngressWebUI: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref SubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 CidrIp: !Sub ${UserGIP} SubVPCLocustSecurityGroupIngressMasterSlave: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref SubVPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5558 CidrIp: !Sub ${UserGIP} ECSTaskRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - sts:AssumeRole Path: / ECSTaskRolePolicy: Type: AWS::IAM::Policy Properties: PolicyName: AllowDynamoDB PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: dynamodb:* Resource: "*" Roles: - !Ref ECSTaskRole ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess ECRRepository: Type: AWS::ECR::Repository Properties: RepositoryName: hogehoge/locust LocustPubCluster: Type: AWS::ECS::Cluster Properties: ClusterName: LocustPubCluster LocustSubCluster: Type: AWS::ECS::Cluster Properties: ClusterName: LocustSubCluster EcsInstancePubAsg: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - Fn::Join: - ',' - - !Ref PubVPCPrivateSubnetA LaunchConfigurationName: !Ref PubInstanceLc MinSize: 0 MaxSize: 100 DesiredCapacity: 0 EcsInstanceSubAsg: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - Fn::Join: - ',' - - !Ref SubVPCPrivateSubnetA LaunchConfigurationName: !Ref SubInstanceLc MinSize: 0 MaxSize: 100 DesiredCapacity: 0 PubInstanceLc: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: ami-0254e5972ebcd132c InstanceType: m5.large IamInstanceProfile: !Ref EC2InstanceProfile SecurityGroups: - !Ref PubVPCLocustSecurityGroup BlockDeviceMappings: - DeviceName: /dev/xvdcz Ebs: VolumeType: gp2 VolumeSize: 32 UserData: Fn::Base64: Fn::Sub: | #!/bin/bash echo ECS_CLUSTER=LocustPubCluster >> /etc/ecs/ecs.config echo ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs"]' >> /etc/ecs/ecs.config echo ECS_ENABLE_TASK_IAM_ROLE=true >> /etc/ecs/ecs.config echo ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true >> /etc/ecs/ecs.config yum update -y SubInstanceLc: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: ami-0254e5972ebcd132c InstanceType: m5.large IamInstanceProfile: !Ref EC2InstanceProfile SecurityGroups: - !Ref SubVPCLocustSecurityGroup BlockDeviceMappings: - DeviceName: /dev/xvdcz Ebs: VolumeType: gp2 VolumeSize: 32 UserData: Fn::Base64: Fn::Sub: | #!/bin/bash echo ECS_CLUSTER=LocustSubCluster >> /etc/ecs/ecs.config echo ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs"]' >> /etc/ecs/ecs.config echo ECS_ENABLE_TASK_IAM_ROLE=true >> /etc/ecs/ecs.config echo ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true >> /etc/ecs/ecs.config yum update -y EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: [!Ref 'EC2Role'] EC2Role: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ec2.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: ['ecs:CreateCluster', 'ecs:DeregisterContainerInstance', 'ecs:DiscoverPollEndpoint', 'ecs:Poll', 'ecs:RegisterContainerInstance', 'ecs:StartTelemetrySession', 'ecs:Submit*', 'logs:CreateLogStream', 'logs:PutLogEvents'] Resource: '*' LocustPubMasterTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 512 Family: locust-pub-master Memory: 1GB NetworkMode: awsvpc RequiresCompatibilities: - FARGATE TaskRoleArn : !GetAtt ECSTaskRole.Arn ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--master" - "-f" - "pub.py" Image: !Sub - '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}' - {RepoName: !Ref ECRRepository} Name: locust-pub-master LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref PubMasterLog awslogs-stream-prefix: !Ref PubMasterLog LocustPubSlaveTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 512 Family: locust-pub-slave Memory: 1GB NetworkMode: bridge RequiresCompatibilities: - EC2 TaskRoleArn : !GetAtt ECSTaskRole.Arn ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--slave" - "-f" - "pub.py" - "--master-host" - "pub-master.local" Environment: - Name: IOT_ENDPOINT Value: xxxxxx.iot.us-east-1.amazonaws.com - Name: SEQ_TABLE Value: seq_table Image: !Sub - '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}' - {RepoName: !Ref ECRRepository} Name: locust-pub-slave LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref PubSlaveLog awslogs-stream-prefix: !Ref PubSlaveLog LocustSubMasterTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 512 Family: locust-sub-master Memory: 1GB NetworkMode: awsvpc RequiresCompatibilities: - FARGATE TaskRoleArn : !GetAtt ECSTaskRole.Arn ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--master" - "-f" - "sub.py" Image: !Sub - '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}' - {RepoName: !Ref ECRRepository} Name: locust-sub-master LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref SubMasterLog awslogs-stream-prefix: !Ref SubMasterLog LocustSubSlaveTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 512 Family: locust-sub-slave Memory: 1GB NetworkMode: bridge RequiresCompatibilities: - EC2 TaskRoleArn : !GetAtt ECSTaskRole.Arn ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--slave" - "-f" - "sub.py" - "--master-host" - "sub-master.local" Environment: - Name: IOT_ENDPOINT Value: xxxxxx.iot.us-east-1.amazonaws.com - Name: SEQ_TABLE Value: seq_table - Name: AWS_ACCESS_KEY_ID Value: xxxxxxxxxx - Name: AWS_SECRET_ACCESS_KEY Value: xxxxxxxxxx Image: !Sub - '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}' - {RepoName: !Ref ECRRepository} Name: locust-sub-slave LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref SubSlaveLog awslogs-stream-prefix: !Ref SubSlaveLog LocustPubMasterService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustPubCluster.Arn DesiredCount: 0 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref PubVPCLocustSecurityGroup Subnets: - !Ref PubVPCPublicSubnetA ServiceName: locust-pub-master TaskDefinition: !Ref LocustPubMasterTaskDef LocustPubSlaveService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustPubCluster.Arn DesiredCount: 0 LaunchType: EC2 ServiceName: locust-pub-slave TaskDefinition: !Ref LocustPubSlaveTaskDef LocustSubMasterService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustSubCluster.Arn DesiredCount: 0 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref SubVPCLocustSecurityGroup Subnets: - !Ref SubVPCPublicSubnetA ServiceName: locust-sub-master TaskDefinition: !Ref LocustSubMasterTaskDef LocustSubSlaveService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustSubCluster.Arn DesiredCount: 0 LaunchType: EC2 ServiceName: locust-sub-slave TaskDefinition: !Ref LocustSubSlaveTaskDef SeqTable: Type: AWS::DynamoDB::Table Properties: TableName: seq_table AttributeDefinitions: - AttributeName: type AttributeType: S KeySchema: - AttributeName: type KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 PubMasterLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/pub-master PubSlaveLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/pub-slave SubMasterLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/sub-master SubSlaveLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/sub-slave Outputs: LocustPubCluster: Description: LocustPubCluster Value: !GetAtt LocustPubCluster.Arn LocustSubCluster: Description: LocustSubCluster Value: !GetAtt LocustSubCluster.Arn PubVPCSecurityGroup: Description: PubVPCSecurityGroup Value: !Ref PubVPCLocustSecurityGroup PubVPCPublicSubnet: Description: PubVPCPublicSubnet Value: !Ref PubVPCPublicSubnetA PubVPCPrivateSubnet: Description: PubVPCPrivateSubnet Value: !Ref PubVPCPrivateSubnetA SubVPCSecurityGroup: Description: SubVPCSecurityGroup Value: !Ref SubVPCLocustSecurityGroup SubVPCPublicSubnet: Description: SubVPCPublicSubnet Value: !Ref SubVPCPublicSubnetA SubVPCPrivateSubnet: Description: SubVPCPrivateSubnet Value: !Ref SubVPCPrivateSubnetA LocustPubMasterTaskDef: Description: LocustPubMasterTaskDef Value: !Ref LocustPubMasterTaskDef LocustPubSlaveTaskDef: Description: LocustPubSlaveTaskDef Value: !Ref LocustPubSlaveTaskDef LocustSubMasterTaskDef: Description: LocustSubMasterTaskDef Value: !Ref LocustSubMasterTaskDef LocustSubSlaveTaskDef: Description: LocustSubSlaveTaskDef Value: !Ref LocustSubSlaveTaskDef
コンテナイメージのビルド&プッシュ
リソースが作成できたらDockerイメージをビルドしてECRにプッシュします
docker build -t hogehoge/locust . $(aws ecr get-login --no-include-email --region us-east-1) docker tag hogehoge/locust:latest xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/hogehoge/locust:latest docker push xxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/hogehoge/locust:latest
DynamoDBへのアイテムPUT
連番採番用のテーブルに必要なアイテムをPUTしておきます
aws dynamodb put-item --table-name seq_table --item '{"type": {"S": "pub"}, "seq": {"N": "0"}}' --region us-east-1 aws dynamodb put-item --table-name seq_table --item '{"type": {"S": "sub"}, "seq": {"N": "0"}}' --region us-east-1
負荷テスト実施
実際にテストを実施していく手順です。
Publish用masterを起動
Publish用のmasterを起動します。
AWSマネジメントコンソールからサービスにECSを選択 クラスター:LocustPubCluster
のサービスlocust-pub-master
を選択し、タスク数を0から1に変更します。
タスクが起動するとIPアドレス(パブリック,プライベート)が付与されるので控えておきます。
Subscribe用masterを起動
Publish用のmasterと同様の手順で起動します。
Publish用slaveを起動
次にslaveを起動して行きます。
タスク定義の修正
タスク定義を修正し、環境変数等を適切に設定します。
AWSマネジメントコンソールのメニュー「タスク定義」からlocust-pub-slave
を開き、最新のリビジョンを選択した状態から「新しいリビジョンの作成」をクリック
次にタスク定義の詳細が開くので、コンテナlocust-pub-slave
を選択し、コマンドの末尾でmasterサーバーの指定がpub-master.localとなっている箇所を、先ほど起動したmasterコンテナのプライベートIPに変更します。ここはうまくサービスディスカバリを使って自動化したかったのですが、CloudFormationで設定できないようだったので、今回は手作業でやることにしました。
合わせて環境変数IOT_ENDPOINT
を負荷テスト対象のエンドポイントに設定します。
EC2の起動
マネジメントコンソール等から、Auto Scaling グループlocust-EcsInstancePubAsg-xxxxxの
設定値を変更しEC2を起動させます。
slave用サービスのタスク数調整
master用サービスと同様にslave用のタスク数を変更します。 この際タスク定義のリビジョンを先ほど修正した最新版のタスク定義に変更して下さい。
Subscribe用slaveを起動
Publish用のslaveと同様の手順で起動して行きます。
追加で環境変数AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
にそれぞれアクセスキーIDとシークレットアクセスキーを設定して下さい。
シナリオ実行
Publish・Subscribe両方の準備ができたので、実際にテストを実行して行きます。
http://<Publish用masterのGIP>:8089 にアクセスし、シミュレーションしたいユーザー数とHatch Rate(1秒間に何ユーザー起動するか)を入力します。 なお、画面右上には現在接続されているslaveの台数が表示されます。
同じようにSubscribe側でもテストを実行開始し、しばらく待つと・・・・
テスト状況が表示されました!! 後はシナリオで決めた時間だけテストを流して「Download Data」のタブから各種のデータをDLして分析すればOKです。
まとめ
Locustを使用した負荷テストについて見て来ました。
今回実施したシナリオだと、ネットワーク周りがボトルネックになっている様で(詳細は調査中です)、1コンテナあたり約330ユーザー程度、EC2インスタンス1台(というよりENI1個)あたりを3コンテナを超えたあたりから、AWS IoTへの接続失敗が頻発しだしました。
そのため、ユーザー数は1コンテナあたり300、EC2毎のコンテナ数は3を目安にスケールさせていったのですが、EC2のインスタンス数の上限に引っかかり、2万接続のシナリオが実施できませんでした。 2万接続超のシナリオを実施する場合はEC2の上限緩和申請が必要になるのでご留意下さい。 また、同時接続数をさらに上げていくと今は見えていないボトルネックやAWSのサービス上限に引っかかる可能性もあります。 もしこのブログを参考に負荷テストを実施する場合、構成については十分に検討して頂く様お願いします。 また、上限緩和申請とは別に、AWSへの相談も忘れずにお願いします。
参考
下記のサイトを参考にさせて頂きました